﻿namespace Auros.EPiSiteMap
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Xml;

    using Auros.EPiSiteMap.Data;

    using EPiServer;
    using EPiServer.BaseLibrary.Scheduling;
    using EPiServer.Core;
    using EPiServer.DataAbstraction;
    using EPiServer.Licensing;
    using EPiServer.PlugIn;
    using EPiServer.Web;
    using System.Globalization;

    [ScheduledPlugIn(DisplayName = "Sitemap")]
    public class SitemapJob : JobBase
    {
        String sTimeFormat = "yyyy-MM-ddThh:mm:sszzz";

        /// <summary>
        ///   Gets all the pages to be included in the sitemap.
        /// </summary>
        private PageDataCollection GetAllPages(PageReference startPage, string[] languages)
        {
            PageDataCollection pages = new PageDataCollection();

            foreach (var language in languages)
            {
                // Add the descendants for the language
                pages.Add(GetAllPagesforLanguage(startPage, language));
            }

            return pages;
        }

        /// <summary>
        ///   Gets all for a language included in the sitemap.
        /// </summary>
        private PageDataCollection GetAllPagesforLanguage(PageReference startPage, string language)
        {
            PageDataCollection pages = new PageDataCollection();

            // Get the language descendants
            PropertyCriteriaCollection criterias = new PropertyCriteriaCollection();

            PropertyCriteria langCriteria = new PropertyCriteria();
            langCriteria.Name = "PageLanguageBranch";
            langCriteria.Required = true;
            langCriteria.Type = PropertyDataType.String;
            langCriteria.Value = language;

            criterias.Add(langCriteria);

            pages.Add(DataFactory.Instance.FindPagesWithCriteria(startPage, criterias, language));

            // Add the start page as this will not be included
            PageData startPageData = DataFactory.Instance.GetPage(startPage, new LanguageSelector(language));

            if (startPageData != null)
            {
                pages.Add(startPageData);
            }

            return pages;
        }

        /// <summary>
        ///   Filters the pages against the sitemap filters.
        /// </summary>
        private PageDataCollection FilterPages(PageDataCollection pages)
        {
            foreach (var filter in Auros.EPiSiteMap.Filters.FilterLocator.GetSitemapFilters())
            {
                filter.Filter(pages);
            }

            return pages;
        }

        /// <summary>
        ///   Creates the sitemap file.
        /// </summary>
        private bool CreateSitemap(SiteMapHostConfig hostConfig, out SiteMapResult result)
        {
            var host = SiteInformationHandler.GetSiteInformation(
                hostConfig.SiteID, false).SiteHosts.FirstOrDefault(h => h.SiteHost == hostConfig.Host);

            // 1. Get the start page for the site and the pages to be displayed in the sitemap.
            int startPageId = SiteInformationHandler.GetSiteInformation(hostConfig.SiteID, false).StartPageId;
            PageReference startPage = new PageReference(startPageId);

            PageDataCollection pages = new PageDataCollection();

            // Add pages from the default language for the host.
            if (!string.IsNullOrEmpty(host.Language))
            {
                pages.Add(GetAllPages(startPage, new string[] { host.Language }));
            }

            // Add any additional languages which have been specified via config.
            pages.Add(GetAllPages(startPage, hostConfig.GetEnabledLanguageSelections()));

            // Filter the pages
            pages = FilterPages(pages);

            // 2. Create header of the sitemap file
            XmlDocument xDoc = new XmlDocument();

            XmlNode xNode = xDoc.CreateXmlDeclaration("1.0", "utf-8", null);

            xDoc.AppendChild(xNode);

            XmlNode xURLSet = xDoc.CreateElement("urlset");
            XmlAttribute xXMLNS = xDoc.CreateAttribute("xmlns");
            xXMLNS.Value = "http://www.sitemaps.org/schemas/sitemap/0.9";

            xURLSet.Attributes.Append(xXMLNS);
            xDoc.AppendChild(xURLSet);

            Int32 iCounter = 0;

            // 3. Create the page nodes
            foreach (PageData page in pages)
            {
                var friendlyUrl = GetFriendlyUrl(page, host);

                XmlComment xComment = xDoc.CreateComment(
                    string.Format("{0} ({1}) - {2}",
                        page.PageName, page.PageLink.ID, page.LanguageBranch));

                XmlNode xURLNode = xDoc.CreateElement("url");
                XmlNode xLoc = xDoc.CreateElement("loc");
                XmlNode xURL = xDoc.CreateTextNode(friendlyUrl);

                xURLNode.AppendChild(xComment);

                xLoc.AppendChild(xURL);
                xURLNode.AppendChild(xLoc);

                XmlNode xLastMod = xDoc.CreateElement("lastmod");
                xLastMod.AppendChild(xDoc.CreateTextNode(page.Saved.ToString(sTimeFormat)));
                xURLNode.AppendChild(xLastMod);

                XmlNode xChangeFrequency = xDoc.CreateElement("changefreq");
                xChangeFrequency.AppendChild(xDoc.CreateTextNode("daily"));
                xURLNode.AppendChild(xChangeFrequency);

                XmlNode xPriority = xDoc.CreateElement("priority");
                xPriority.AppendChild(xDoc.CreateTextNode(
                    GetPriority(friendlyUrl).ToString("0.0", CultureInfo.CreateSpecificCulture("en-US"))));
                xURLNode.AppendChild(xPriority);

                xURLSet.AppendChild(xURLNode);

                iCounter++;
            }

            // 4. Save the file
            string savePath = ResolveUrl(hostConfig.FileName);

            xDoc.Save(savePath);

            // Set the return params
            SiteMapResult sitemapResult = new SiteMapResult();
            sitemapResult.Host = hostConfig.Host;
            sitemapResult.NumberOfPages = iCounter;
            sitemapResult.Created = DateTime.Now;
            sitemapResult.Files = new string[] { hostConfig.FileName };

            result = sitemapResult;

            return true;
        }

        /// <summary>
        ///   Creates the sitemap index file.
        /// </summary>
        private void CreateSitemapIndexFile(string indexFileName, List<SiteMapResult> sitemaps)
        {
            XmlDocument xDoc = new XmlDocument();

            XmlNode xNode = xDoc.CreateXmlDeclaration("1.0", "utf-8", null);

            xDoc.AppendChild(xNode);

            XmlNode xSitemapIndexSet = xDoc.CreateElement("sitemapindex");
            XmlAttribute xXMLNS = xDoc.CreateAttribute("xmlns");
            xXMLNS.Value = "http://www.sitemaps.org/schemas/sitemap/0.9";

            xSitemapIndexSet.Attributes.Append(xXMLNS);
            xDoc.AppendChild(xSitemapIndexSet);

            foreach (var sitemap in sitemaps)
            {
                foreach (var file in sitemap.Files)
                {
                    XmlNode xSiteMapNode = xDoc.CreateElement("sitemap");

                    // Create the address to the sitemap file
                    var address = string.Format("{0}://{1}/{2}", "http", sitemap.Host, file);

                    XmlNode xLocNode = xDoc.CreateElement("loc");
                    xLocNode.AppendChild(xDoc.CreateTextNode(address));

                    XmlNode xLastModNode = xDoc.CreateElement("lastmod");
                    xLastModNode.AppendChild(xDoc.CreateTextNode(sitemap.Created.ToString(sTimeFormat)));

                    xSiteMapNode.AppendChild(xLocNode);
                    xSiteMapNode.AppendChild(xLastModNode);

                    xSitemapIndexSet.AppendChild(xSiteMapNode);
                }
            }

            // Save the file
            string savePath = ResolveUrl(indexFileName);

            xDoc.Save(savePath);
        }

        /// <summary>
        ///   Gets the friendly url for the page
        /// </summary>
        private string GetFriendlyUrl(PageData page, HostSetting host)
        {
            string result = string.Empty;

            UrlBuilder relativeUrl;

            // Sometimes, the LinkUL contains the protocol and domain. Cannot figure out why - its very annoying.
            // Therefore, we shall use a URL builder object and take the path and query to form a relative URL.
            var pageUrl = new UrlBuilder(page.LinkURL);

            string relativeLinkUrl = string.Concat(pageUrl.Path, pageUrl.Query);

            if (page.LinkType != PageShortcutType.External)
            {
                relativeUrl = new UrlBuilder(relativeLinkUrl);

                relativeUrl.QueryCollection.AddOrReplace("epslanguage", page.LanguageBranch);

                // The logic for converting to an external relies upon http context for the host of the website the url we are calculating.
                // Two things can happen here.
                //  1. When creating the sitemaps by manually running the scheduled job, the current hostname (being used to access admin) is used.
                //  2. When running as a scheduled job (from the windows service), no http context exists.

                // Therefore we need to cater for either the wrong host in the current http context or no http context at all!

                // If the host of the current request is not for the sitemap we are generating, change it!.
                // This occurs when using enterprise setup where logged into one website but generating sitemaps (on-demand) for all sites.
                if (System.Web.HttpContext.Current != null && System.Web.HttpContext.Current.Request.Headers["host"] != host.SiteHost)
                {
                    System.Web.HttpContext.Current.Request.Headers["host"] = host.SiteHost;
                }

                if (UrlRewriteProvider.IsFurlEnabled)
                {
                    EPiServer.Global.UrlRewriteProvider.ConvertToExternal(relativeUrl, page.PageLink, Encoding.UTF8);
                }

                // If the re-write has occurred with no http context, the URL will be prefixed with a language code or prefix segment as there is no context available to check the host culture.
                // We can check for this and remove it if still present.
                if (page.LanguageID == host.Language)
                {
                    string pathPrefix = "/" + LanguageBranch.Load(host.Language).CurrentUrlSegment + "/";

                    if (relativeUrl.Path.StartsWith(pathPrefix))
                    {
                        relativeUrl.Path = "/" + relativeUrl.Path.Remove(0, pathPrefix.Length);
                    }
                }

                result = "http://" + host.SiteHost + relativeUrl.ToString();
            }
            else
            {
                result = new UrlBuilder(relativeLinkUrl).ToString();
            }

            return result;
        }

        /// <summary>
        ///   Resolves a relative Url path to an absolute disk path based on the website base directory for the web application.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private string ResolveUrl(string path)
        {
            String resolvedPath = path;

            if (path.StartsWith("/"))
            {
                // We cant rely on http context as when running as a schedule, it could be null.
                // Instead, we can use EPiServer.Global.BaseDirectory which return the directory of the websites root.
                // Therefore we need to

                // 1. Remove the '/' from the relative web path
                resolvedPath = path.Replace("/", string.Empty);
            }

            // 2. Replace the URL directory identifies '/' to UNC directory path identifiers '\'
            resolvedPath = resolvedPath.Replace('/', '\\');

            resolvedPath = System.IO.Path.Combine(EPiServer.Global.BaseDirectory, resolvedPath);

            return resolvedPath;
        }

        /// <summary>
        ///   Gets the priority.
        /// </summary>
        private double GetPriority(string url)
        {
            int depth = new Uri(url).Segments.Count() - 1;

            return Math.Max(1.0 - (depth / 10.0), 0.5);
        }

        /// <summary>
        ///   Runs the job
        /// </summary>
        public override string Execute()
        {
            Int32 filesCreated = 0;

            List<SiteMapResult> results = new List<SiteMapResult>();

            // Create the sitemaps files
            foreach (var item in SiteMapHostConfig.GetSitesAndHosts())
            {
                this.OnStatusChanged(String.Format("Creating sitemap for ({0}) {1}", item.SiteID, item.Host));

                SiteMapResult result;
                bool created = CreateSitemap(item, out result);

                // Update the DDS record for the last creation time and number of items created.
                item.LastCreated = DateTime.Now;
                item.NumberOfEntries = result.NumberOfPages;

                filesCreated += result.Files.Length;

                results.Add(result);

                SiteMapHostConfig.Save(item);
            }

            // Create the index file
            var config = SiteMapConfig.GetConfig();
            if (config.CreateIndexFile)
            {
                this.CreateSitemapIndexFile(config.SitemapIndexFileName, results);
            }

            return String.Format("{0} sitemap file(s) created.", filesCreated);
        }
    }

    /// <summary>
    ///   A structure to represent the results of creating a sitemap for a host entry in EPiServer.
    /// </summary>
    public struct SiteMapResult
    {
        /// <summary>
        ///   Gets or sets the creation date of the sitemap file.
        /// </summary>
        public DateTime Created { get; set; }

        /// <summary>
        ///   Gets or sets the hostname for the sitemap file.
        /// </summary>
        public string Host { get; set; }

        /// <summary>
        ///   Gets or sets the number of pages in the sitemap.
        /// </summary>
        public int NumberOfPages { get; set; }

        /// <summary>
        ///   Gets or sets the files created for the sitemap.
        /// </summary>
        public string[] Files { get; set; }
    }
}